/*#######################################################################
# Version3/CSI
# http://www.ver3.com
# support@ver3.com
#
# Copyright (c) 2009 CSI Technology Outfitters / Version3
#
# Date: February 20, 2009
# Author: Steven Kean
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
#########################################################################*/

#region using Statements

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Xml;
using System.Text.RegularExpressions;
using System.Text;
using System.Reflection;
using System.Collections.Specialized;
using Microsoft.MetadirectoryServices;

using Version3.ILM.ManagementAgents.SharePoint.WebServices;

#endregion

namespace Version3.ILM.ManagementAgents.SharePoint {

	public class SharePointCE : IMAExtensibleFileImport, IMAExtensibleCallExport {

		#region ListField class

		/// <summary>
		/// Class used to store SharePoint list field information.
		/// </summary>
		private class ListField {
			public string ID;
			public string Name;
			public string DisplayName;
			public string Type;
		}

		#endregion

		#region Declarations

		private string _Username = string.Empty;
		private string _Password = string.Empty;
		private string _Domain = string.Empty;
		private string _SiteURL = string.Empty;
		private string _ListName = string.Empty;
		private List<ListField> _ListFields = null;
		private Lists _ListsWebService = null;
		private bool _UseDisplayNames = true;

		#endregion

		#region Initialize method

		/// <summary>
		/// Initializes the global variables used during import and export.
		/// </summary>
		/// <param name="connectTo">The connect to information specified in the management agent configuration.</param>
		/// <param name="user">The username specified in the management agent configuration.</param>
		/// <param name="password">The password specified in the management agent configuration.</param>
		/// <param name="configParameters">The configuration parameter collection for the management agent.</param>
		private void Initialize( string connectTo, string user, string password, ConfigParameterCollection configParameters ) {
			// trim any trailing slashes from URL, as we add one later
			_SiteURL = connectTo.TrimEnd( '/' );
			_Username = user;
			if ( user.Contains( "\\" ) ) {
				// the username contains a domain, parse it off
				_Domain = user.Substring( 0, user.IndexOf( "\\" ) );
				_Username = user.Substring( user.IndexOf( "\\" ) + 1 );
			}
			_Password = password;
			
			// the SharePoint list name
			_ListName = GetConfigParameter( configParameters, "SharePoint_List_Name" );
			
			// indicates whether the list fields specified in the management agent configuration
			// should be considered display names or internal field names
			// defaults to true, since the internal field names may not be obvious
			_UseDisplayNames = bool.Parse( GetConfigParameter( configParameters, "Use_DisplayNames", "true" ) );
			
			// attempt to connect to the Lists web service with the specifed connect to information
			_ListsWebService = GetListsWebService();
			
			// retrieve the SharePoint list fields
			_ListFields = GetListFields( _ListsWebService, _ListName );
		}

		#endregion

		#region IMAExtensibleFileImport implementation

		public void GenerateImportFile( string filename, string connectTo,
			string user, string password, ConfigParameterCollection configParameters,
			bool fullImport, TypeDescriptionCollection types, ref string customData ) {

			// initialize our configuration options and connections
			Initialize( connectTo, user, password, configParameters );

			try {
				// ensure that the listItem object type is defined in the management agent
				TypeDescription listItemType;
				try {
					listItemType = types[ "listItem" ];
				} catch ( NoSuchObjectTypeException ) {
					throw new UnexpectedDataException( "listItem is a required object type for the SharePoint Lists MA." );
				}

				// ensure that the ID field is specified in the attributes of the listItem object type
				bool foundID = false;
				foreach ( AttributeDescription attrib in listItemType.Attributes ) {
					if ( attrib.Name.Equals( "ID" ) ) {
						foundID = true;
					}
				}
				if ( !foundID ) {
					throw new UnexpectedDataException( "ID is a required attribute for the SharePoint Lists MA." );
				}

				// generate the XML that specifies the fields to be retrieved from the SharePoint list
				XmlDocument xmlDoc = new XmlDocument();
				XmlElement xmlFields = xmlDoc.CreateElement( "ViewFields" );
				// loop through the defined attributes of the listItem object type, and add them to the query
				foreach ( AttributeDescription attrib in listItemType.Attributes ) {
					string field = attrib.Name.Trim();
					if ( !field.Equals( "Delta" ) ) {
						// retrieve the ListField object so the internal SharePoint name can be
						// determined if display names were used
						ListField listField = GetListField( _ListFields, field, _UseDisplayNames );
						if ( listField != null ) {
							XmlElement xmlFieldRef = xmlDoc.CreateElement( "FieldRef" );
							xmlFieldRef.SetAttribute( "Name", listField.Name );
							xmlFields.AppendChild( xmlFieldRef );
						} else {
							throw new UnexpectedDataException( "Invalid field name specified: " + field );
						}
					}
				}

				// last import token is stored in the custom data, 
				// retrieve it if this is not a full import
				string sinceToken = null;
				if ( !fullImport ) {
					sinceToken = customData;
				}
				// query for the first 1,000,000 records that have changed since the last delta import
				// TODO: we query for 1,000,000 to avoid having to deal with paging, but this should be expanded
				// to support paged lists
				XmlNode itemsXml = _ListsWebService.GetListItemChangesSinceToken( _ListName, null, null, xmlFields, "1000000", null, sinceToken, null );

				// Build AVP import file
				StringBuilder listItemData = new StringBuilder();
				StringBuilder referenceData = new StringBuilder();
				List<string> referenceObjects = new List<string>();

				// setup XML namespace manager for parsing the list XML
				XmlNamespaceManager nsExtMgr = new XmlNamespaceManager( itemsXml.OwnerDocument.NameTable );
				nsExtMgr.AddNamespace( "soap", itemsXml.NamespaceURI );
				nsExtMgr.AddNamespace( "rs", "urn:schemas-microsoft-com:rowset" );
				nsExtMgr.AddNamespace( "z", "#RowsetSchema" );

				// retrieve the Changes node
				XmlNode xmlChanges = itemsXml.SelectSingleNode( "soap:Changes", nsExtMgr );
				if ( xmlChanges != null ) {
					// retrieve and store the LastChangeToken for delta import processing
					XmlAttribute xmlChangeToken = xmlChanges.Attributes[ "LastChangeToken" ];
					if ( xmlChangeToken != null ) {
						customData = xmlChangeToken.Value;
					}
					// for delta queries, deletes are listed under the Changes node, so
					// loop through the deletes and add them to the import file
					XmlNodeList xmlDeletes = xmlChanges.SelectNodes( "soap:Id[@ChangeType='Delete']", nsExtMgr );
					foreach ( XmlNode xmlDelete in xmlDeletes ) {
						listItemData.Append("ObjectType: listItem\r\n");
						listItemData.AppendFormat( "ID: {0}\r\n", xmlDelete.InnerText );
						listItemData.Append( "Delta: Delete\r\n" );
						listItemData.Append( "\r\n" );
					}
				}

				// grab the item data and loop through each row
				XmlNode xmlData = itemsXml.SelectSingleNode( "rs:data", nsExtMgr );
				if ( xmlData != null ) {
					XmlNodeList xmlRows = xmlData.SelectNodes( "z:row", nsExtMgr );

					foreach ( XmlNode xmlRow in xmlRows ) {
						listItemData.Append( "ObjectType: listItem\r\n" );

						// loop through the attributes defined on the listItem object type
						foreach ( AttributeDescription attrib in listItemType.Attributes ) {
							string field = attrib.Name.Trim();

							if ( !field.Equals( "Delta" ) ) {

								// look for the specifed attribute in the ListFields list
								ListField listField = GetListField( _ListFields, field, _UseDisplayNames );
								if ( listField != null ) {
									// the field exists in the list, so grab it from the result XML
									if ( xmlRow.Attributes[ "ows_" + listField.Name ] != null ) {
										string value = xmlRow.Attributes[ "ows_" + listField.Name ].Value;
										string[] values;

										// determine the field type and process it accordingly
										switch ( listField.Type ) {
											case "User":
											case "UserMulti":
												// userlist, so let's retrieve data from UserInfo list
												values = GetUsernamesFromUserList( value );
												for ( int i = 0; i < values.Length; i++ ) {
													if ( attrib.DataType == AttributeType.Reference ) {
														// this is a Reference field in ILM, so we create a reference
														// entry for the import file
														if ( !referenceObjects.Contains( values[ i ] ) ) {
															referenceData.Append( "ObjectType: reference\r\n" );
															referenceData.AppendFormat( "ID: {0}\r\n", values[ i ] );
															referenceData.Append( "\r\n" );
															referenceObjects.Add( values[ i ] );
														}
													}
													listItemData.AppendFormat( "{0}: {1}\r\n", field, values[ i ] );
												}
												break;
											case "LookupMulti":
												// split on the sharepoint lookup field delimiter
												values = Regex.Split( value, ";#" );
												for ( int i = 0; i < values.Length; i++ ) {
													// skip the ID value and add the lookup value
													if ( i % 2 == 1 ) {
														if ( attrib.DataType == AttributeType.Reference ) {
															// this is a Reference field in ILM, so we create a reference
															// entry for the import file
															if ( !referenceObjects.Contains( values[ i ] ) ) {
																referenceData.Append( "ObjectType: reference\r\n" );
																referenceData.AppendFormat( "ID: {0}\r\n", values[ i ] );
																referenceData.Append( "\r\n" );
																referenceObjects.Add( values[ i ] );
															}
														}
														listItemData.AppendFormat( "{0}: {1}\r\n", field, values[ i ] );
													}
												}
												break;
											case "MultiChoice":
												// split on the sharepoint multi-choice field delimiter
												values = Regex.Split( value, ";#" );
												for ( int i = 0; i < values.Length; i++ ) {
													listItemData.AppendFormat( "{0}: {1}\r\n", field, values[ i ] );
												}
												break;
											default:
												// nothing special about this field (as far as we know) so process
												// it as plain text
												listItemData.AppendFormat( "{0}: {1}\r\n", field, value );
												break;
										}
									} else {
										// no field was found in the XML, so assume it's and empty value
										listItemData.AppendFormat( "{0}: \r\n", field );
									}
								} else {
									// the field was not found in the list
									throw new UnexpectedDataException( "Invalid field name specified: " + field );
								}
							}
						}
						// Delta column
						if ( !fullImport ) {
							// ILM is smart enough to treat modifies as adds if they don't
							// exist, so list all deltas as modifies
							listItemData.Append( "Delta: Modify\r\n" );
						}
						listItemData.Append( "\r\n" );
					}
				}
				// write the import data out to file
				using ( StreamWriter sw = new StreamWriter( filename ) ) {
					if ( referenceData.Length > 0 ) {
						sw.Write( referenceData.ToString() );
					}
					sw.Write( listItemData.ToString() );
					sw.Close();
				}
			} catch ( System.Web.Services.Protocols.SoapException ex ) {
				// an error we weren't expecting occurred, so pull it from the
				// soap response and throw it
				throw ( new UnexpectedDataException( ex.Detail.InnerText ) );
			}
		}

		#endregion

		#region IMAExtensibleCallExport implementation

		public void BeginExport( string connectTo, string user, string password,
			ConfigParameterCollection configParameters, TypeDescriptionCollection types ) {
			// initialize our configuration options and connections
			Initialize( connectTo, user, password, configParameters );
		}

		public void ExportEntry( ModificationType modificationType, string[] changedAttributes, CSEntry csentry ) {
			// we only export listItem objects
			if ( csentry.ObjectType.Equals( "listItem" ) ) {
				try {

					// generate the CAML for the list update
					XmlDocument xmlDoc = new XmlDocument();
					XmlElement xmlUpdates = xmlDoc.CreateElement( "Batch" );
					XmlElement xmlMethod = xmlDoc.CreateElement( "Method" );

					// we're exporting one object at a time, so the method ID is always 1
					xmlMethod.SetAttribute( "ID", "1" );

					// add the Cmd attribute based on the modification type
					switch ( modificationType ) {
						case ModificationType.Add:
							xmlMethod.SetAttribute( "Cmd", "New" );
							break;
						case ModificationType.Replace:
							xmlMethod.SetAttribute( "Cmd", "Update" );
							break;
						case ModificationType.Delete:
							xmlMethod.SetAttribute( "Cmd", "Delete" );
							break;
					}

					xmlUpdates.AppendChild( xmlMethod );

					// only process if it's an Add or an ID is present
					if ( modificationType == ModificationType.Add || ( csentry[ "ID" ].IsPresent && !string.IsNullOrEmpty( csentry[ "ID" ].Value ) ) ) {
						XmlElement xmlField = xmlDoc.CreateElement( "Field" );
						xmlField.SetAttribute( "Name", "ID" );
						if ( modificationType == ModificationType.Add ) {
							// for new items, the ID is "New"
							xmlField.InnerText = "New";
						} else {
							// for modifies, use the ID
							xmlField.InnerText = csentry[ "ID" ].Value;
						}
						xmlMethod.AppendChild( xmlField );

						// if it's a delete, we're done, otherwise add the updated field values
						if ( modificationType != ModificationType.Delete ) {
							foreach ( string attrib in changedAttributes ) {
								ListField listField = GetListField( _ListFields, attrib, _UseDisplayNames );
								if ( listField != null ) {
									xmlField = xmlDoc.CreateElement( "Field" );
									xmlField.SetAttribute( "Name", listField.Name );

									string value = csentry[ attrib ].Value;
									if ( listField.Type.Equals( "User", StringComparison.InvariantCultureIgnoreCase ) ) {
										int testValue;
										if ( !int.TryParse( value, out testValue ) ) {
											// we don't have an id, so we need to get one
											value = GetUserID( value );
										}
									} else if ( listField.Type.Equals( "UserMulti", StringComparison.InvariantCultureIgnoreCase ) ) {
										// TODO: figure out how to handle UserMulti for export
										throw new UnexpectedDataException( "UserMulti fields are not supported for export." );
									}
									xmlField.InnerText = value;
									xmlMethod.AppendChild( xmlField );
								}
							}
						}

						// Send the update CAML to SharePoint
						XmlNode xmlResult = _ListsWebService.UpdateListItems( _ListName, xmlUpdates );

						// Parse the results
						XmlNamespaceManager nsMgr = new XmlNamespaceManager( xmlResult.OwnerDocument.NameTable );
						nsMgr.AddNamespace( "soap", xmlResult.NamespaceURI );
						XmlNode xmlError = xmlResult.SelectSingleNode( "soap:Result/soap:ErrorCode", nsMgr );
						if ( xmlError != null && xmlError.InnerText != "0x00000000" ) {
							// an error occurred, so put it in a friendlier format and throw it
							string error = xmlError.InnerText;
							xmlError = xmlResult.SelectSingleNode( "soap:Result/soap:ErrorText", nsMgr );
							if ( xmlError != null ) {
								error += ": " + xmlError.InnerText;
							}
							throw ( new EntryExportException( error ) );
						}
					} else {
						// an ID is required for updates
						throw ( new AttributeNotPresentException( "ID" ) );
					}
				} catch ( Exception ex ) {
					throw ( new EntryExportException( ex.Message ) );
				}
			} else {
				// there's either an export flow configured for the "reference" object type
				// or another object type has been configured.  Neither is supported.
				throw new UnexpectedDataException( csentry.ObjectType + " is not supported for export flow." );
			}
		}

		public void EndExport() {
			// do nothing
		}

		#endregion

		#region SharePoint Communication

		/// <summary>
		/// Retrieves a reference to the Lists web service.
		/// </summary>
		private Lists GetListsWebService() {
			// build the URL for the Lists web service
			string listsURL = _SiteURL + "/_vti_bin/Lists.asmx";
			Lists listsWS = new Lists( listsURL );
			if ( string.IsNullOrEmpty( _Username ) ) {
				// no username and password were defined in the management agent configuration,
				// so use ILM's security context
				listsWS.UseDefaultCredentials = true;
			} else {
				// create NetworkCredentials from the specified connect to information
				listsWS.Credentials = new System.Net.NetworkCredential( _Username, _Password, _Domain );
			}
			try {
				// make a generic call to verify credentials
				listsWS.GetListCollection();
			} catch ( WebException ex ) {
				if ( ex.InnerException != null && ex.InnerException.Message.Contains( "401" ) ) {
					// a 401 will be thrown if the credentials are invalid
					throw new BadServerCredentialsException();
				} else {
					// not sure what happened, so throw the error
					throw;
				}
			}
			return listsWS;
		}

		/// <summary>
		/// Retrieves a reference to the UserGroup web service.
		/// </summary>
		private UserGroup GetUsersWebService() {
			// build the URL for the UserGroup web service
			string usersURL = _SiteURL + "/_vti_bin/UserGroup.asmx";
			UserGroup usersWS = new UserGroup( usersURL );
			if ( string.IsNullOrEmpty( _Username ) ) {
				// no username and password were defined in the management agent configuration,
				// so use ILM's security context
				usersWS.UseDefaultCredentials = true;
			} else {
				// create NetworkCredentials from the specified connect to information
				usersWS.Credentials = new System.Net.NetworkCredential( _Username, _Password, _Domain );
			}
			try {
				// make a generic call to verify credentials
				usersWS.GetGroupCollectionFromSite();
			} catch ( WebException ex ) {
				if ( ex.InnerException.Message.Contains( "401" ) ) {
					// a 401 will be thrown if the credentials are invalid
					throw new BadServerCredentialsException();
				} else {
					// not sure what happened, so throw the error
					throw;
				}
			}
			return usersWS;
		}

		/// <summary>
		/// Retrieves the SharePoint list schema returns a list of ListFields.
		/// </summary>
		/// <param name="listsWS">The Lists web service reference.</param>
		/// <param name="listName">The name of the list to retrieve.</param>
		/// <returns>A list of ListField objects.</returns>
		private List<ListField> GetListFields( Lists listsWS, string listName ) {
			// grab the list schema from the Lists web service
			XmlNode listXml = listsWS.GetList( listName );

			// set up the XML namespace manager for parsing of the schema XML
			XmlNamespaceManager nsMgr = new XmlNamespaceManager( listXml.OwnerDocument.NameTable );
			nsMgr.AddNamespace( "soap", listXml.NamespaceURI );

			XmlDocument xmlDoc = new XmlDocument();
			List<ListField> fields = new List<ListField>();

			// parse the list schema and populate the ListField list
			XmlNodeList xmlFields = listXml.SelectNodes( "soap:Fields/soap:Field", nsMgr );
			foreach ( XmlNode xmlField in xmlFields ) {
				ListField field = new ListField();
				// ID of the field
				field.ID = xmlField.Attributes[ "ID" ].Value;

				// internal SharePoint name of the field
				field.Name = xmlField.Attributes[ "Name" ].Value;

				// display name of the field
				field.DisplayName = xmlField.Attributes[ "DisplayName" ].Value;

				// value type of the field
				field.Type = xmlField.Attributes[ "Type" ].Value;
				fields.Add( field );
			}
			return fields;
		}

		/// <summary>
		/// Retrieves a ListField object from a list using either the field name or the display name.
		/// </summary>
		/// <param name="listFields">The list of ListField objects.</param>
		/// <param name="name">The name of the ListField to return.</param>
		/// <param name="isDisplayName">Boolean indicating whether the "name" parameter is a display name.</param>
		/// <returns>A ListField object.</returns>
		private ListField GetListField( List<ListField> listFields, string name, bool isDisplayName ) {
			foreach ( ListField listField in listFields ) {
				if ( ( isDisplayName && listField.DisplayName.Equals( name, StringComparison.InvariantCultureIgnoreCase ) ) ||
					 ( !isDisplayName && listField.Name.Equals( name, StringComparison.InvariantCultureIgnoreCase ) ) ) {
					return listField;
				}
			}
			return null;
		}

		/// <summary>
		/// Retrieves the SharePoint UserInfo list ID for the specified user.
		/// </summary>
		/// <param name="ntAccountName">The NT account name of the user.</param>
		/// <returns>The SharePoint UserInfo list ID of the user.  Returns an empty string if the user is not found.</returns>
		private string GetUserID( string ntAccountName ) {
			try {
				// retrieve the UserGroup web service
				UserGroup usersWS = GetUsersWebService();

				// retrieve the user XML
				XmlNode xmlUserInfo = usersWS.GetUserInfo( ntAccountName );

				// setup the XML namespace manager for parsing the user XML
				XmlNamespaceManager nsMgr = new XmlNamespaceManager( xmlUserInfo.OwnerDocument.NameTable );
				nsMgr.AddNamespace( "soap", xmlUserInfo.NamespaceURI );

				// find the User node
				XmlNode xmlUser = xmlUserInfo.SelectSingleNode( "soap:User", nsMgr );
				if ( xmlUser != null ) {
					// we found the user, so return the ID
					return xmlUser.Attributes[ "ID" ].Value;
				}

				// no user was found, so return an empty string
				return string.Empty;
			} catch ( System.Web.Services.Protocols.SoapException ex ) {
				// an error occurred, so throw a new error with the details
				throw ( new UnexpectedDataException( ex.Detail.InnerText ) );
			}
		}

		/// <summary>
		/// Parses a SharePoint user list into a string array of usernames.
		/// </summary>
		/// <param name="userList">The SharePoint user list to parse.</param>
		/// <returns>A string array of usernames.</returns>
		private string[] GetUsernamesFromUserList( string userList ) {
			// 1;#FirstName LastName,#Domain\Username,#Username@Domain,#,#FirstName LastName;#2;#...
			// Aside from the ID, we can't be certain of the fields available in the user list
			// returned from SharePoint, so we need to query the UserInfo list to get the account
			// names

			string usernameField = "Name"; //Name is the account name
			List<string> users = new List<string>();
			if ( !String.IsNullOrEmpty( userList ) ) {
				// set up the CAML query to return the user information
				XmlDocument xmlDoc = new XmlDocument();

				XmlElement xmlFields = xmlDoc.CreateElement( "ViewFields" );
				XmlElement xmlFieldRef = xmlDoc.CreateElement( "FieldRef" );
				xmlFieldRef.SetAttribute( "Name", usernameField );
				xmlFields.AppendChild( xmlFieldRef );

				XmlElement xmlQueryOptions = xmlDoc.CreateElement( "QueryOptions" );
				XmlElement xmlQuery = xmlDoc.CreateElement( "Query" );
				XmlElement xmlWhere = xmlDoc.CreateElement( "Where" );
				xmlQuery.AppendChild( xmlWhere );

				// loop through the users in the user list build the conditions CAML
				XmlElement xmlCondition = null;
				int userCount = 0;
				string[] userInfo = Regex.Split( userList, ";#" );
				foreach ( string info in userInfo ) {
					// the IDs should be the only parsable integers in the string
					int id;
					if ( int.TryParse( info, out id ) ) {
						userCount++;

						XmlElement xmlEqual = xmlDoc.CreateElement( "Eq" );
						XmlElement xmlParam = xmlDoc.CreateElement( "FieldRef" );
						xmlParam.SetAttribute( "Name", "ID" );
						xmlEqual.AppendChild( xmlParam );
						xmlParam = xmlDoc.CreateElement( "Value" );
						xmlParam.SetAttribute( "Type", "Counter" );
						xmlParam.InnerText = info;
						xmlEqual.AppendChild( xmlParam );

						if ( userCount > 1 ) {
							// we're retrieving more than one user, so we need to
							// add Or clauses to the query
							XmlElement xmlOr = xmlDoc.CreateElement( "Or" );
							xmlOr.AppendChild( xmlCondition );
							xmlOr.AppendChild( xmlEqual );
							xmlCondition = xmlOr;
						} else {
							xmlCondition = xmlEqual;
						}
					}
				}

				// append the conditions CAML to the query
				if ( xmlCondition != null ) {
					xmlWhere.AppendChild( xmlCondition );
				}

				// query the UserInfo SharePoint list
				XmlNode xmlUsers = _ListsWebService.GetListItems( "UserInfo", null, xmlQuery, xmlFields, userCount.ToString(), xmlQueryOptions, null );

				// parse the XML results
				XmlNamespaceManager nsMgr = new XmlNamespaceManager( xmlUsers.OwnerDocument.NameTable );
				nsMgr.AddNamespace( "soap", xmlUsers.NamespaceURI );
				nsMgr.AddNamespace( "rs", "urn:schemas-microsoft-com:rowset" );
				nsMgr.AddNamespace( "z", "#RowsetSchema" );

				// loop through the data nodes, and grab the username field value (ows_Name)
				// and add it to the users array
				XmlNode xmlData = xmlUsers.SelectSingleNode( "rs:data", nsMgr );
				if ( xmlData != null ) {
					XmlNodeList xmlRows = xmlData.SelectNodes( "z:row", nsMgr );
					foreach ( XmlNode xmlRow in xmlRows ) {
						if ( xmlRow.Attributes[ "ows_" + usernameField ] != null ) {
							users.Add( xmlRow.Attributes[ "ows_" + usernameField ].Value );
						}
					}
				}
			}
			return users.ToArray();
		}

		#endregion

		#region GetConfigParameter

		/// <summary>
		/// Retrieves an ILM management agent configuration parameter.
		/// </summary>
		/// <param name="configParameters">The configParameters collection.</param>
		/// <param name="paramName">The name of the parameter to retrieve.</param>
		/// <returns>The string value of the configuration parameter.</returns>
		private string GetConfigParameter( ConfigParameterCollection configParameters, string paramName ) {
			string value = GetConfigParameter( configParameters, paramName, null );
			if ( value == null || string.IsNullOrEmpty( value ) ) {
				throw ( new UnexpectedDataException( "Parameter: " + paramName + " is missing or blank" ) );
			}
			return value;
		}

		/// <summary>
		/// Cleanly retrieves an ILM management agent configuration parameter.  Returning a default value if parameter is not found.
		/// </summary>
		/// <param name="configParameters">The configParameters collection.</param>
		/// <param name="paramName">The name of the parameter to retrieve.</param>
		/// <param name="defaultValue">The default value to return if the parameter is null or empty.</param>
		/// <returns>The string value of the configuration parameter, or the default value if the parameter is not found.</returns>
		private string GetConfigParameter( ConfigParameterCollection configParameters, string paramName, string defaultValue ) {
			try {
				ConfigParameter param = configParameters[ paramName ];
				if ( param == null || string.IsNullOrEmpty( param.Value ) ) {
					return defaultValue;
				}
				return param.Value;
			} catch {
				return defaultValue;
			}
		}

		#endregion
	}
}
